// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using ILLink.Shared;
using ILLink.Shared.TypeSystemProxy;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace ILLink.RoslynAnalyzer
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public sealed class COMAnalyzer : DiagnosticAnalyzer
    {
        private const string StructLayoutAttribute = nameof(StructLayoutAttribute);
        private const string DllImportAttribute = nameof(DllImportAttribute);
        private const string MarshalAsAttribute = nameof(MarshalAsAttribute);

        private static readonly DiagnosticDescriptor s_correctnessOfCOMCannotBeGuaranteed = DiagnosticDescriptors.GetDiagnosticDescriptor(DiagnosticId.CorrectnessOfCOMCannotBeGuaranteed,
            helpLinkUri: "https://learn.microsoft.com/dotnet/core/deploying/trim-warnings/il2050");

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(s_correctnessOfCOMCannotBeGuaranteed);

        public override void Initialize(AnalysisContext context)
        {
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);

            if (!System.Diagnostics.Debugger.IsAttached)
                context.EnableConcurrentExecution();

            context.RegisterCompilationStartAction(context =>
            {
                var compilation = context.Compilation;
                if (!context.Options.IsMSBuildPropertyValueTrue(MSBuildPropertyOptionNames.EnableTrimAnalyzer))
                    return;

                context.RegisterOperationAction(operationContext =>
                {
                    var invocationOperation = (IInvocationOperation)operationContext.Operation;
                    var targetMethod = invocationOperation.TargetMethod;
                    if (!targetMethod.HasAttribute(DllImportAttribute))
                        return;

                    if (operationContext.ContainingSymbol.IsInRequiresUnreferencedCodeAttributeScope(out _))
                        return;

                    bool comDangerousMethod = IsComInterop(targetMethod.ReturnType);
                    foreach (var parameter in targetMethod.Parameters)
                    {
                        comDangerousMethod |= IsComInterop(parameter);
                    }

                    if (comDangerousMethod)
                    {
                        operationContext.ReportDiagnostic(Diagnostic.Create(s_correctnessOfCOMCannotBeGuaranteed,
                            operationContext.Operation.Syntax.GetLocation(), targetMethod.GetDisplayName()));
                    }
                }, OperationKind.Invocation);
            });

            static bool IsComInterop(ISymbol symbol)
            {
                if (symbol.TryGetAttribute(MarshalAsAttribute, out var marshalAsAttribute) &&
                    marshalAsAttribute.ConstructorArguments.Length >= 1 && marshalAsAttribute.ConstructorArguments[0] is TypedConstant typedConstant &&
                    typedConstant.Type != null && typedConstant.Type.IsUnmanagedType)
                {
                    var unmanagedType = typedConstant.Value;
                    switch (unmanagedType)
                    {
                        case (int)UnmanagedType.IUnknown:
                        case (int)UnmanagedType.IDispatch:
                        case (int)UnmanagedType.Interface:
                            return true;

                        default:
                            if (Enum.IsDefined(typeof(UnmanagedType), unmanagedType))
                                return false;

                            break;
                    }
                }

                if (symbol.IsInterface())
                    return true;

                ITypeSymbol? typeSymbol = symbol is ITypeSymbol ? symbol as ITypeSymbol : null;
                if (symbol is IParameterSymbol parameterSymbol)
                    typeSymbol = parameterSymbol.Type;

                if (typeSymbol is IPointerTypeSymbol)
                    return false;

                if (typeSymbol == null)
                    return false;

                if (typeSymbol.IsTypeOf(WellKnownType.System_Array))
                {
                    // System.Array marshals as IUnknown by default
                    return true;
                }
                else if (typeSymbol.IsTypeOf(WellKnownType.System_String) ||
                    typeSymbol.IsTypeOf("System.Text", "StringBuilder"))
                {
                    // String and StringBuilder are special cased by interop
                    return false;
                }

                if (typeSymbol.IsValueType)
                {
                    // Value types don't marshal as COM
                    return false;
                }
                else if (typeSymbol.IsInterface())
                {
                    // Interface types marshal as COM by default
                    return true;
                }
                else if (typeSymbol.IsTypeOf("System", "MulticastDelegate"))
                {
                    // Delegates are special cased by interop
                    return false;
                }
                else if (typeSymbol.IsSubclassOf("System.Runtime.InteropServices", "CriticalHandle") ||
                    typeSymbol.IsSubclassOf("System.Runtime.InteropServices", "SafeHandle"))
                {
                    // Subclasses of CriticalHandle and SafeHandle are special cased by interop
                    return false;
                }
                else if (typeSymbol.TryGetAttribute(StructLayoutAttribute, out var structLayoutAttribute) &&
                    (LayoutKind)structLayoutAttribute.ConstructorArguments[0].Value! == LayoutKind.Auto)
                {
                    // Rest of classes that don't have layout marshal as COM
                    return true;
                }

                return false;
            }
        }
    }
}
